Загрузка библиотек:
import torch
import torchvision
import torchvision.transforms as transforms
import torch.nn as nn
# import torch.optim as optim
#import os
# import torch.nn.functional as F
import matplotlib.pyplot as plt
# from collections import defaultdict # to check distribution by classes
# from sklearn.metrics import precision_recall_fscore_support # to calculate F1 score
# from sklearn.model_selection import StratifiedShuffleSplit # to split images to train, val, and test
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\tqdm\auto.py:22: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html from .autonotebook import tqdm as notebook_tqdm
Загружаем лучший стейт обученной модели
transform = transforms.Compose([
transforms.Resize(64),
transforms.CenterCrop(64),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
image_dataset = torchvision.datasets.ImageFolder('images', transform=transform)
image_dataloader = torch.utils.data.DataLoader(image_dataset, batch_size=32, shuffle=False)
model = torchvision.models.resnet18(weights='ResNet18_Weights.IMAGENET1K_V1')
model.fc = nn.Linear(512, len(image_dataset.classes))
model.load_state_dict(torch.load('best_model_42_epoch_01_last.pt'))
image_classes = image_dataset.classes
Заменяем последний полносвязный уровень на единицы, чтобы извлечь признаки предпоследнего уровня для каждого изображения
model.fc_backup = model.fc
# model.fc = nn.Sequential()
model.fc = nn.Identity()
model.eval()
all_features = []
with torch.no_grad():
for inputs, labels in image_dataloader:
outputs = model(inputs)
features = outputs
all_features.append(features)
all_features = torch.cat(all_features, dim=0)
print(all_features)
print(all_features.size())
labels = np.concatenate([batch[1].numpy() for batch in image_dataloader])
tensor([[1.3572, 1.6053, 0.1278, ..., 2.3075, 0.6772, 3.0499],
[6.0935, 2.7589, 0.0000, ..., 3.3483, 4.4636, 0.0000],
[0.7585, 0.0000, 0.5369, ..., 0.4460, 0.0000, 0.0000],
...,
[0.0000, 0.0000, 1.3359, ..., 0.0670, 2.0176, 0.0000],
[0.0000, 1.7225, 2.1514, ..., 0.0000, 0.0000, 1.4539],
[0.0422, 2.4869, 1.3453, ..., 0.8349, 0.2712, 0.0000]])
torch.Size([1422, 512])
Полученная матрица (тензор) feature vector имеет размерность количество изображений (1422) * количество нейронов (512) на последнем уровне.
Сначала попробуем понизить размерность: используем PCA.
# используем StandardScaler для шкалирования наших данных
sc = StandardScaler()
X_scaled = sc.fit_transform(all_features)
# Apply PCA
pca = PCA(n_components=None)
pca.fit(X_scaled)
# Get the eigenvalues
# print("Eigenvalues:")
# print(pca.explained_variance_)
# print()
# Get explained variances
# print("Variances (Percentage):")
# print(pca.explained_variance_ratio_ * 100)
# print()
# Make the scree plot
plt.plot(np.cumsum(pca.explained_variance_ratio_ * 100))
plt.xlabel("Number of components (Dimensions)")
plt.ylabel("Explained variance (%)")
plt.hlines(y = 60, xmin = 0, xmax = 512, colors = 'g', linestyles = '--')
plt.show()
Можно увидеть, что первые главные компоненты объясняют достаточно мало дисперсии. Это значит, что признаки получились мало коррелированы между собой. Попробуем сделать кластеризацию методом к-средних на исходной матрице, количество кластеров выбираем 8, как и число классов изображения:
from sklearn.cluster import KMeans
kmeans = KMeans(n_clusters=8, random_state=42)
kmeans.fit(all_features)
labels_kmeans = kmeans.labels_
centers = kmeans.cluster_centers_
from sklearn.metrics import adjusted_rand_score
labels == labels_kmeans
ari = adjusted_rand_score(labels, labels_kmeans)
print("Adjusted Rand Index: {:.4f}".format(ari))
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\cluster\_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning warnings.warn(
Adjusted Rand Index: 0.1536
Для оценки точности используем функцию adjusted_rand_score, которая сама определяет наиболее вероятный лейбл кластера. Точность кластеризации получилась не очень высокая. Для визуализации и отрисовки лейблов используем UMAP.
import umap.umap_ as umap
embedding = umap.UMAP(n_neighbors=15,
min_dist=0.3,
metric='correlation').fit_transform(all_features)
# Plot the UMAP visualization with true labels
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
# axes[1].colorbar()
# plt.colorbar(ax=axes)
fig.suptitle('k-means based on the original matrix, ARI = {:.4f}'.format(ari))
plt.show()
# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with true labels')
# plt.show()
# # Plot the UMAP visualization with predicted labels
# plt.scatter(embedding[:, 0], embedding[:, 1], c=labels_kmeans, cmap='tab10', s = 6)
# plt.colorbar()
# plt.title('UMAP visualization with predicted labels')
# plt.show()
Попробуем использовать алгоритм к-средних на матрице из первых 100 главных компонент
# from sklearn.decomposition import PCA
pca = PCA(n_components=100)
pca_data = pca.fit_transform(all_features)
# Perform k-means clustering on the PCA components
kmeans_pca = KMeans(n_clusters=8, random_state=42)
kmeans_pca.fit(pca_data)
# Print the cluster labels assigned to each data point
print(kmeans_pca.labels_)
ari_pca = adjusted_rand_score(labels, kmeans_pca.labels_)
print("Adjusted Rand Index: {:.4f}".format(ari_pca))
fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(12, 6))
axes[0].scatter(embedding[:, 0], embedding[:, 1], c=labels, cmap='tab10', s=6)
axes[0].set_title('UMAP visualization with true labels')
# axes[0].colorbar()
# Plot the UMAP visualization with predicted labels
axes[1].scatter(embedding[:, 0], embedding[:, 1], c=kmeans_pca.labels_, cmap='tab10', s=6)
axes[1].set_title('UMAP visualization with predicted by kmeans labels')
fig.suptitle('k-means based on first 100 PC, ARI = {:.4f}'.format(ari_pca))
# axes[1].colorbar()
# plt.colorbar(ax=axes)
plt.show()
C:\Users\elena\AppData\Local\Programs\Python\Python310\lib\site-packages\sklearn\cluster\_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning warnings.warn(
[5 3 5 ... 4 7 5] Adjusted Rand Index: 0.1568
Точность кластеризации не улучшилась после использования главных компонент, возможно потому что исходные переменные не были сильно скоррелированы друг с другом. Попробуем использовать иерархическую кластеризацию
# import numpy as np
from scipy.cluster.hierarchy import dendrogram, linkage, fcluster
# Perform hierarchical clustering
Z = linkage(all_features, method='ward')
# Cut the dendrogram to 8 groups
labels_hier = fcluster(Z, 8, criterion='maxclust')
color_threshold = Z[-7, 2]
# Plot the dendrogram to visualize the hierarchy
dendrogram(Z, color_threshold=color_threshold)
plt.title('Dendrogram')
plt.xlabel('Sample index')
plt.ylabel('Distance')
plt.show()
# Compare the resulting labels with the true labels
score = adjusted_rand_score(labels, labels_hier)
print('Adjusted Rand score:', score)
Adjusted Rand score: 0.1625178276870089
Теперь попробуем использовать метод k-ближайших соседей. Загрузим те же самые индексы тренировочного датасета и тестового + валидационного, чтобы избежать утечки данных.
train_index = np.load('train_index_42.npy')
train_index
test_index = np.load('test_index_42.npy')
print(test_index)
X_train = all_features[train_index]
X_test = all_features[test_index]
y_train = labels[train_index]
y_test = labels[test_index]
[ 640 295 1277 734 897 1304 79 955 908 116 252 660 1187 716
1382 1017 1147 1121 663 1077 1181 592 822 990 353 927 1262 430
530 1311 391 778 515 1172 396 1397 1162 1190 1079 35 1018 1049
1010 967 291 618 503 826 1282 277 1416 1076 1324 722 1066 1111
1313 1275 605 375 771 357 266 192 747 165 590 810 619 307
670 934 553 1 558 675 936 788 981 784 280 1207 656 325
567 916 344 180 451 198 1150 940 880 92 966 607 97 1280
697 621 347 1153 42 458 10 615 893 931 498 947 7 193
1045 1056 668 1084 1003 860 172 100 1052 878 657 235 145 704
118 653 1320 600 951 129 1071 410 1240 1235 1288 112 885 316
1323 1059 1182 1194 1388 1095 439 383 1296 673 1264 652 236 1136
1087 650 243 52 1041 1270 1286 939 109 509 323 914 633 1417
424 1168 701 958 389 278 841 126 328 699 576 324 1409 57
305 106 200 814 876 494 1373 1242 993 195 1384 93 1126 1244
1009 105 906 69 402 473 522 862 39 905 1127 163 639 614
272 898 802 270 297 973 360 1048 350 433 262 51 282 1254
2 635 1141 573 463 462 715 720 77 203 710 725 365 355
185 523 779 903 1091 21 863 1239 1099 1291 201 127 787 513
1368 1113 767 702 589 597 674 114 855 984 28 1369 130 1053
768 417 170 1133 514 529 1319 334 1367 431 1258 1391 1051 1119
18 696 1241 1272 1006 770 638 1261 1294 1047 824 583 255 764
452 1166 332 1022 405 1396 611 774 818 279 234 1335 998 1040
729 286 289 752 441 755 1130 1131 1176 569 1404 1158 557 254
224 1177 1144 54 1380 427 429 1274 1410 445 672 1033 942 803
782 1050 912 1152 1339 1214 22 1307 409 207 271 570 99 374
14 624 301 368 1201 229 251 1352 815 728 680 398 1322 335
160 381 620 1024 108 1309 1408 1300 1316 608 609 525 468 1101
476 1257 679 692 38 321 1266 983 496 581 265 695 751 724
1299 1353 649 472 227 732 20 73 453 23 861 319 505 226
418 626 754 66 1357 798 985 1215 1179 281 938 150 117 1385
847 221 830 1358 199 1008 1210]
from sklearn.neighbors import KNeighborsClassifier
# from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report
# X_train, X_test, y_train, y_test = train_test_split(all_features, labels, test_size=0.2, random_state=66)
# k = 16 # сработало хорошо
k = 15
knn = KNeighborsClassifier(n_neighbors=k)
knn.fit(X_train, y_train)
y_pred = knn.predict(X_test)
report = classification_report(y_test, y_pred, zero_division=0)
print(report)
acc = np.mean(y_pred == y_test)
print("Classification accuracy: {:.2f}%".format(acc * 100))
precision recall f1-score support
0 1.00 0.16 0.27 19
1 0.55 0.92 0.69 110
2 0.57 0.71 0.63 72
3 0.76 0.21 0.33 61
4 0.57 0.66 0.61 65
5 0.69 0.69 0.69 35
6 1.00 0.22 0.36 23
7 0.72 0.31 0.43 42
accuracy 0.59 427
macro avg 0.73 0.48 0.50 427
weighted avg 0.66 0.59 0.55 427
Classification accuracy: 59.25%
Метод k-ближайших соседей отработал лучше, чем k-means, поэтому попробуем визуализировать картинки с соответствующим лейблом. По очереди отрисуем картинки, соответствующие предсказанным лейблам.
def show_images(indices, nrow=4,ncol=4, title = ''):
images = [image_dataset[idx][0] for idx in indices]
curr_labels = labels[indices]
curr_labels = (np.take(image_classes, curr_labels))
fig, axs = plt.subplots(nrows=nrow, ncols=ncol, figsize=(21, 12))
# plt.title('title')
# iterate over the subplots and plot the images and labels
for i, ax in enumerate(axs.flat):
# plot the image
imgs = images[i].numpy().transpose((1, 2, 0))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
imgs = std * imgs + mean
imgs = np.clip(imgs, 0, 1)
ax.imshow(imgs)
ax.set_title(str(curr_labels[i]))
ax.axis("off")
fig.suptitle(title, fontsize=18)
plt.show()
test_index[y_pred == 0]
array([52, 28, 23], dtype=int64)
show_images(test_index[y_pred == 0], 1, len(test_index[y_pred == 0]), 'Для артдеко характерно наличие картинки по центру, по краям фон')
Первый класс изображений - предсказанный алгоритмом к-ближайших соседей АртДеко. Определилось три изображения, все относятся к нужному классу. Можно отметить наличие изображения по центру, в то время как по краям фон. Возможно, это являлось признаком, на которые обращает внимание модель. Отрисуем следующий класс изображений: кубизм
show_images(test_index[y_pred == 1], 6, 6, 'Для кубизма сложно найти ярко выраженные фичи')
Видно, что многие изображения определились правильно, однако очень много ложно-позитивных срабатываний. В отличие от предыдущего случая, здесь изображения заполнены содержанием по всей площади. Цвета яркие. Я смотрела сама изображения в папке кубизм, туда попал в том числе Сезанн, который скорее в стиле постимпрессионизма пишет. Например, здесь это изображение во второй строке четвертого столбца (paul-cezanne_chateau-noir-1). Подобные изображения могли затруднить идентификацию признаков изображений классического кубизма (Малевич). Перейдем к импрессионизму
print(len(test_index[y_pred == 2]))
show_images(test_index[y_pred == 2], 6, 6, 'Для импрессионизма характерны изображения природы')
90
Особенности картин, которые классифицированы как импрессионизм ярко выделяются: изображения природы, деревьев, озер. По этой причине натурализм и фотографии, где изображена природа ошибочно классифицированы как импрессионизм. Например 5 строка 1 столбец, фотография на неискушенный глаз похожа на картину импрессионистов, особенно при таком низком разрешении. Так что неудивительно, что нейросеть тоже ошиблась :)
print(len(test_index[y_pred == 3]))
show_images(test_index[y_pred == 3], 4, 4, 'Для японизма возможно ключевым признаком является бежевый фон')
17
Кажется что нейросеть определелиа для японизма характерным свойством бежевый светлый фон, возможно как в той истории про то как волков от собак отличали по снежному фону у волков. Это объясняет почему сюда попал кубизм (2 строка 3 столбец), птичка (3, 3) и розовая бутылка (3, 2)
show_images(test_index[y_pred == 4], 6, 6, 'В натурализме много цветов и растений')
Для натурализма характерны такие формы как цветы и листья. Есть несколько странных ложнопозитивных срабатываний, например машина, что сложно интерпретировать.
show_images(test_index[y_pred == 5], 6, 5, 'Большинство изображений стиля рококо содержат лица')
Одно из самых ярких признаков - Рококо. Почти на всех изображениях есть человек, в особенности лицо. Это объясняет почему фотографии с лицами людей определились в рококо. Кроме того, любопытно что нейросеть распознает и группы людей, где лица некрупным планом. Можно сказать, что для нейросети определящим признаком, что изображение относится к рококо, является наличие людей.
show_images(test_index[y_pred == 6], 1, len(test_index[y_pred == 6]), 'Для комиксов (cartoon) характерны яркие цвета')
Картинок с мультиками немного, но они корректно определились. Характерным свойством является яркие, даже слишком цвета. К сожалению, выборка в исходном датасете небольшая.
print(len(test_index[y_pred == 7],))
show_images(test_index[y_pred == 7], 3, 6, image_classes[7])
18
Похоже для нейросети важным признаком фотографии являются машинки, мотициклы, в целом транспорт и коты. Некоторые изображения определились достаточно неочевидно, например артдеко и японизм, которых ложно записали сюда. Но в целом логика прослеживается.
Можно заметить, что извлеченные изображения достаточно контрастные, хотя кубизм оказался достаточно шумным, без ярко выраженных фичей. Возможно, стоит увеличить выборку.
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.metrics import accuracy_score
from sklearn.metrics import confusion_matrix, f1_score
# Create a gradient boosting classifier
gb = GradientBoostingClassifier(n_estimators=100, learning_rate=0.1, max_depth=3, random_state=42)
# Train the classifier on the training set
gb.fit(X_train, y_train)
# Make predictions on the test set
y_pred = gb.predict(X_test)
confusion_matrix_gb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_gb)
# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix: [[ 3 8 0 6 2 0 0 0] [ 1 93 4 3 2 0 2 5] [ 0 8 50 2 9 3 0 0] [ 0 12 3 38 4 0 1 3] [ 2 6 11 4 35 2 1 4] [ 0 0 6 0 8 20 0 1] [ 0 4 7 4 1 1 5 1] [ 0 5 6 1 4 4 1 21]] Accuracy: 0.6206, F1-score wieghted: 0.6051
dataframe = pd.DataFrame(confusion_matrix_gb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))
# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of gradient boosting"), plt.tight_layout()
plt.ylabel("True Class"),
plt.xlabel("Predicted Class")
plt.show()
Точность и F1-score алгоритма градиентного бустинга чуть ниже чем у нейронной сети. Для нейросети в этом наборе параметров получилось: Точность: 0.6842, Weighted F1-Score: 0.6811 Попробую использовать алгоритм xgboost
import xgboost as xgb
xgb_model = xgb.XGBClassifier(n_estimators=100, learning_rate=0.3, max_depth=3, random_state=42)
# Train the model
xgb_model.fit(X_train, y_train)
# Make predictions on the test set
y_pred = xgb_model.predict(X_test)
# Calculate the confusion matrix
confusion_matrix_xgb = confusion_matrix(y_test, y_pred)
print('Confusion Matrix:')
print(confusion_matrix_xgb)
# Calculate F1 score
f1 = f1_score(y_test, y_pred, average='weighted')
# print(f'F1 score: {f1:.3f}')
# Calculate the accuracy of the model
accuracy = accuracy_score(y_test, y_pred)
# print(f'Accuracy: {accuracy:.3f}')
print('Accuracy: {:.4f}, F1-score wieghted: {:.4f}'.format(accuracy, f1))
Confusion Matrix: [[ 5 9 0 1 2 0 2 0] [ 4 91 3 1 6 0 1 4] [ 0 13 43 2 10 3 1 0] [ 1 10 4 38 4 1 0 3] [ 1 5 9 5 39 2 0 4] [ 0 0 5 0 7 23 0 0] [ 0 4 4 5 0 0 8 2] [ 0 5 4 0 5 2 0 26]] Accuracy: 0.6393, F1-score wieghted: 0.6324
dataframe = pd.DataFrame(confusion_matrix_xgb, index=image_classes, columns=image_classes)
plt.figure(figsize=(8, 6))
# Create heatmap
sns.heatmap(dataframe, annot=True, cbar=None,cmap="YlGnBu",fmt="d")
plt.title("Confusion Matrix of xgboost"), plt.tight_layout()
plt.ylabel("True Class"),
plt.xlabel("Predicted Class")
plt.show()
xgboost отработал несколько лучше чем простой градиентный бустинг. Однако softmax в нейросети имеет не меньшую точность и F1 score, по крайней мере при этом наборе параметров.